﻿// Copyright (c) 2011, Adaptiv Design
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation and/or
// other materials provided with the distribution.
//    * Neither the name of the <ORGANIZATION> nor the names of its contributors may
// be used to endorse or promote products derived from this software without specific
// prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.IO;
using System.Linq;

namespace Alicanto.Lexing
{
    public class Lexer
    {
        private static bool _isInitialized = false;

        private static HashSet<char> _structural;
        private static HashSet<string> _operators;
        private static HashSet<string> _literals;
        private static HashSet<string> _commands;
        private static HashSet<string> _blocks;
        private static HashSet<string> _endBlocks;
        private static HashSet<string> _specialNames;

        private const char CR = '\r';
        private const char LF = '\n';
        private const char TAB = '\t';
        private const char SPACE = ' ';

        private const char SINGLE_QOUTE = '\'';
        private const char DOUBLE_QOUTES = '"';
        private const char UNDERSCORE = '_';

        private Lexer()
        {

        }

        /// <summary>
        /// Factory method for creating a lexer
        /// </summary>
        /// <returns></returns>
        static public Lexer Create()
        {
            if (!_isInitialized)
            {
                _isInitialized = true;

                _operators = _collectTokenStrings(TokenType.Operator);
                _commands = _collectTokenStrings(TokenType.Command);
                _blocks = _collectTokenStrings(TokenType.Block);
                _endBlocks = _collectTokenStrings(TokenType.EndBlock);
                _specialNames = _collectTokenStrings(TokenType.SpecialName);
                _literals = _collectTokenStrings(TokenType.Literal);

                // Construct structural hashset
                _structural = new HashSet<char>();
                foreach (string str in _collectTokenStrings(TokenType.Structural))
                    if (str.Length != 1) throw new Exception("Invalid structural token");
                    else _structural.Add(str[0]);

                // Verify settings
                if (Settings.BEGIN_CODE.Length != 2 || Settings.END_CODE.Length != 2)
                    throw new Exception("Invalid template specifiers");

            }

            return new Lexer();
        }

        private static HashSet<string> _collectTokenStrings(TokenType tokenType)
        {
            HashSet<string> strings = new HashSet<string>();

            MemberInfo[] members = typeof(Token).GetMembers(BindingFlags.Public | BindingFlags.Static);
            foreach (MemberInfo member in members)
            {
                if (member is FieldInfo)
                {
                    FieldInfo field = (FieldInfo)member;
                    if (field.FieldType == typeof(Token))
                    {
                        Token token = (Token)field.GetValue(null);
                        if (token.Type == tokenType)
                            strings.Add(token.Str);
                    }
                }
            }

            return strings;
        }

        public TokenSequence Tokenize(string input, bool templateFirst)
        {
            TokenSequence output = new TokenSequence();
            StringReader remainder = new StringReader(input);
            string str = null;

            Action match = new Action(() =>
            {
                if (str.Length == 0)
                    return;

                if (_operators.Contains(str))
                    output.Push(new Token(str, TokenType.Operator));
                else if (_commands.Contains(str))
                    output.Push(new Token(str, TokenType.Command));
                else if (_literals.Contains(str))
                    output.Push(new Token(str, TokenType.Literal));
                else if (_blocks.Contains(str))
                    output.Push(new Token(str, TokenType.Block));
                else if (_endBlocks.Contains(str))
                    output.Push(new Token(str, TokenType.EndBlock));
                else if (_specialNames.Contains(str))
                    output.Push(new Token(str, TokenType.SpecialName));
                else if (str == Token.End.Str)
                    output.Push(new Token(str, TokenType.End));
                else if (str.All((chr) => char.IsLetterOrDigit(chr) || chr == UNDERSCORE))
                    output.Push(new Token(str, TokenType.SimpleName));
                else
                    throw new Exception("Could not parse: " + str);
            });

            Func<char, bool> charIsWhiteSpace = new Func<char, bool>((chr) => 
                chr == SPACE || chr == TAB
            );

            Func<char, bool> charIsEndOfLine = new Func<char, bool>((chr) =>
                chr == CR || chr == LF
            );

            Func<char, bool> charIsStructural = new Func<char, bool>((chr) =>
                _structural.Contains<char>(chr)
            );

            Func<char, bool> charIsTokenEnd = new Func<char, bool>((chr) =>
                charIsWhiteSpace(chr)
                || charIsStructural(chr)
                || charIsEndOfLine(chr)
            );

            Func<char, bool> tryEndToken = new Func<char, bool>((chr) =>
            {
                if (charIsTokenEnd(chr))
                {
                    if (charIsEndOfLine(chr))
                    {
                        match();
                        output.Push(Token.Semicolon);
                        if (remainder.Peek() != -1 && charIsEndOfLine((char)remainder.Peek()))
                            remainder.Read();
                    }
                    else if (charIsStructural(chr))
                    {
                        match();
                        output.Push(new Token(chr, TokenType.Structural));
                    }
                    else
                    {
                        match();
                    }

                    return true;
                }

                return false;
            });

            Func<bool> read = new Func<bool>(() =>
            {
                int val = remainder.Read();
                if (val == -1) return false;

                char chr = (char)val;
                if (tryEndToken(chr))
                    return false;

                str += chr;
                return true;
            });

            // State
            bool isCode = !templateFirst;
            string next = null;

            while (remainder.Peek() != -1)
            {
                if (next != null)
                {
                    str = next;
                    next = null;
                }
                else
                    str = string.Empty;

                if (!isCode)
                {
                    // Template
                    while (remainder.Peek() != -1)
                    {
                        str += (char)remainder.Read();
                        if (str.EndsWith(Settings.BEGIN_CODE))
                            break;
                    }

                    if (str.EndsWith(Settings.BEGIN_CODE))
                    {
                        str = str.Substring(0, str.Length - 2);
                        isCode = true;
                    }

                    if (str.Length > 0)
                        output.Push(new Token(str, TokenType.Template));

                    continue;
                }

                if (str.Length == 0 && !read())
                    continue;

                char chr = str[0];

                // Check for end code
                if (remainder.Peek() != -1 && chr.ToString() + (char)remainder.Peek() == Settings.END_CODE)
                {
                    remainder.Read();
                    isCode = false;
                    continue;
                }

                // Literal (string)
                if (chr == SINGLE_QOUTE || chr == DOUBLE_QOUTES)
                {
                    char quote = chr;
                    while (remainder.Peek() != -1)
                    {
                        chr = (char)remainder.Read();
                        str += chr;

                        if (chr == quote)
                            break;
                    }

                    if (str[str.Length - 1] != quote)
                        throw new Exception("Unclosed string");

                    output.Push(new Token(str, TokenType.Literal));
                    continue;
                }
                // Literal (numerical)
                else if (char.IsNumber(chr))
                {
                    while (remainder.Peek() != -1)
                    {
                        chr = (char)remainder.Peek();
                        if (char.IsNumber(chr) || chr == '.')
                            str += (char)remainder.Read();
                        else
                            break;
                    }

                    double number = 0;
                    if (!double.TryParse(str, out number))
                        throw new Exception("Could not parse number");

                    output.Push(new Token(str, TokenType.Literal));
                    continue;
                }

                while (read()) 
                {
                    char last = str[str.Length - 1];
                    if (char.IsLetter(chr) || chr == UNDERSCORE)
                    {
                        if (char.IsLetterOrDigit(last) || last == UNDERSCORE)
                            // No interruption op alphanumeric sequence... continue on
                            continue;
                    }
                    else
                    {
                        if (!char.IsLetterOrDigit(last) && last != UNDERSCORE && last != SINGLE_QOUTE && last != DOUBLE_QOUTES)
                            // No interruption of operator... continue on
                            continue;
                    }

                    // Either:
                    // Alphanumeric sequence is interrupted by an operator... x+y
                    // OR operator is interrupted by an alphanumeric sequence or quotes... -y;
                    str = str.Substring(0, str.Length - 1);
                    match();

                    next = last.ToString();
                    break;
                }
            }

            return output;
        }
    }
}
